/*
*    l3qd - Light, light, lightweight queue
*    Copyright (C) 2024  Marcus Pedersén marcus@marcux.org
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
	"gorm.io/gorm"
	"log/slog"
	"net"
	"net/http"
	"notabug.org/marcux/l3q/internal/common"
	"notabug.org/marcux/l3q/internal/common/cert"
	"notabug.org/marcux/l3q/internal/daemon"
	"notabug.org/marcux/l3q/internal/daemon/sql"
	"notabug.org/marcux/l3q/internal/daemon/validate"
	"notabug.org/marcux/l3q/internal/daemon/worker"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

type API struct {
	Db *gorm.DB
}

// API for client.
// URL: GET /api/client/job
// Without query parameters all
// jobs are returned.
//
// Query parameters:
// id=x: where x is job id that
// will be returned.
// tasks: To be used with id,
// will return job with id including
// tasks.
// queue: Only parameter, will return
// all queued jobs
// history: Only parameter, will return
// all old jobs that are no longer a part
// of the queue.
// limit=x: x is to limit the number of
// jobs returned in response.
// Can only be combined with history.
// state=x: name of one of the states from
// common.JobState, returns all jobs
// with supplied state.
func (a API) clientGetJob(c *gin.Context) {
	if err := validateHost(c, a.Db, false); err != nil {
		slog.Error(fmt.Sprintf("%s", err))
		return
	}

	id := -1
	tasks := false
	queue := false
	history := false
	correctQuery := false
	stateStr := ""
	state := common.JobState(-1)
	limit := 0

	idStr, idExist := c.GetQuery("id")
	if idExist {
		idInt, err := strconv.Atoi(idStr)
		if err != nil {
			slog.Error(fmt.Sprintf("Request: /api/client/job?id=%s, id is not an integer", idStr))
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter id is not an integer"})
			return
		}
		id = idInt
	}

	_, tExist := c.GetQuery("tasks")
	if tExist {
		tasks = true
	}

	_, qExist := c.GetQuery("queue")
	if qExist {
		queue = true
	}

	_, hExist := c.GetQuery("history")
	if hExist {
		history = true

		limStr, limExist := c.GetQuery("limit")

		if limExist {
			limInt, err := strconv.Atoi(limStr)
			if err != nil {
				slog.Error(fmt.Sprintf("Request: /api/client/job?limit=%s, limit is not an integer", limStr))
				c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter limit is not an integer"})
				return
			}
			limit = limInt
		}
	}

	stateStr, stateExist := c.GetQuery("state")
	if stateExist {
		correctQuery = true

		switch strings.ToLower(stateStr) {
		case strings.ToLower(common.JobStateQueued.String()):
			state = common.JobStateQueued
		case strings.ToLower(common.JobStateRunning.String()):
			state = common.JobStateRunning
		case strings.ToLower(common.JobStateDepend.String()):
			state = common.JobStateDepend
		case strings.ToLower(common.JobStateCancel.String()):
			state = common.JobStateCancel
		case strings.ToLower(common.JobStateTerminated.String()):
			state = common.JobStateTerminated
		case strings.ToLower(common.JobStateCanceled.String()):
			state = common.JobStateCanceled
		case strings.ToLower(common.JobStateError.String()):
			state = common.JobStateError
		case strings.ToLower(common.JobStateNodeError.String()):
			state = common.JobStateNodeError
		default:
			correctQuery = false
		}
	}

	if stateExist && (history || queue || tasks || id > 0) {
		correctQuery = false
	}

	if history && !queue && !tasks && id == -1 && !stateExist {
		correctQuery = true
	}

	if queue && !history && !tasks && id == -1 && !stateExist {
		correctQuery = true
	}

	if id > 0 && !history && !queue && !stateExist {
		correctQuery = true
	}

	if id < 0 && !history && !queue && !tasks && !stateExist {
		correctQuery = true
	}

	if !correctQuery {
		slog.Warn(fmt.Sprintf("Wrong query parameters: %s from client: %s", c.FullPath(), c.ClientIP()))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Wrong query parameters."})
		return
	}

	jobs, err := sql.GetJob(a.Db, id, tasks, state, queue, history, limit)

	if err != nil {
		slog.Error(fmt.Sprintf("Request: /api/client/job, id=%d, %s", id, err))
		c.JSON(http.StatusInternalServerError, common.FailResponse{Message: "Internal error"})
	} else {
		c.JSON(http.StatusOK, jobs)
	}
}

// API for client.
// URL: PATCH /api/client/job/cancel
// Parameters id and user are both required.
//
// Query parameters:
// id=x: where x is job id that
// should be cancelled.
// user=x: where x is user name that
// entered the command on client.
func (a API) clientCancelJob(c *gin.Context) {
	if err := validateHost(c, a.Db, false); err != nil {
		slog.Error(fmt.Sprintf("%s", err))
		return
	}

	id := -1
	user := ""

	idStr, iExist := c.GetQuery("id")
	if iExist {
		idInt, err := strconv.Atoi(idStr)
		if err != nil {
			slog.Error(fmt.Sprintf("Request: /api/client/job/cancel?id=%s, id is not an integer", idStr))
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter id is not an integer"})
			return
		}

		id = idInt
	} else {
		slog.Error("Request: /api/client/job/cancel does not have required parameter: id")
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter id is required"})
		return
	}

	userStr, uExist := c.GetQuery("user")
	if uExist {
		if len(userStr) >= 1 {
			user = userStr
		} else {
			slog.Error("Request: /api/client/job/cancel parameter user can not be an empty string")
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter user can not be an empty string"})
			return
		}
	} else {
		slog.Error("Request: /api/client/job/cancel does not have required parameter: user")
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter user is required"})
		return
	}

	admin := false
	if user == "root" {
		admin = true
	}

	forUser, err := sql.CancelJob(a.Db, id, user, admin)
	if err != nil {
		slog.Error(fmt.Sprintf("Request: /api/client/job/cancel, id=%d, user=%s, %s", id, user, err))
		if forUser {
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: fmt.Sprintf("%s", err)})
		} else {
			c.JSON(http.StatusInternalServerError, common.FailResponse{Message: "Internal error"})
		}
		return
	} else {
		c.JSON(http.StatusOK, gin.H{})
	}
}

// API for client.
// URL: POST /api/client/job
// Post new job in form, this
// function will parse form and
// validate it.
// Job fields:
// Name: (Optional)
// Type: (Required) Sequence or parallel (integer 0 or 1)
// Depend: (Optional) Specify depending jobids, comma separated list
// HostsAlloc: (Required if parallel), if sequence this is set to 1 (one)
// CoresAlloc: Required for job
// User: (Required) User requesting a new job, user running client
// Tasks: (Required) Array of tasks, at minimum one task
func (a API) clientPostJob(c *gin.Context) {
	if err := validateHost(c, a.Db, false); err != nil {
		slog.Error(fmt.Sprintf("%s", err))
		return
	}

	j := common.Job{}

	if err := c.Bind(&j); err != nil {
		slog.Error(fmt.Sprintf("Request: POST /api/client/job, failed to parse job: %s", err))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Failed to parse job"})
		return
	}

	j.Id = 0
	j.StartDate = ""
	j.EndDate = ""
	j.NoTasksRunning = 0
	j.NoTasksFinished = 0
	j.NoTasksErr = 0
	j.Nodes = ""
	j.State = common.JobStateQueued

	now := time.Now()
	j.InitDate = now.Format("2006-01-02 15:04:05")

	if j.NoTasksTotal != len(j.Tasks) {
		j.NoTasksTotal = len(j.Tasks)
	}

	if len(j.Tasks) == 0 {
		slog.Error(fmt.Sprintf("Request: POST /api/client/job, Job does not contain any tasks: %v", j))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Job does not contain any tasks."})
		return
	} else {
		for i := range j.Tasks {
			if len(j.Tasks[i].Command) == 0 {
				slog.Error(fmt.Sprintf("Request: POST /api/client/job, Task in job: %+v command is missing.", j))
				c.JSON(http.StatusBadRequest, common.FailResponse{Message: "No command specified for task in job, command is required"})
				return
			}

			if j.Tasks[i].Seqno <= 0 {
				j.Tasks[i].Seqno = i + 1
			}

			if len(j.Tasks[i].WorkDir) == 0 {
				j.Tasks[i].WorkDir = "$HOME"
			}

			j.Tasks[i].Id = 0
			j.Tasks[i].Unitname = ""
			j.Tasks[i].StartTime = ""
			j.Tasks[i].EndTime = ""
			j.Tasks[i].MonoStartTime = 0
			j.Tasks[i].MonoEndTime = 0
			j.Tasks[i].ActiveState = ""
			j.Tasks[i].SubState = ""
			j.Tasks[i].ExitCode = 0
			j.Tasks[i].MainPid = 0
			j.Tasks[i].MemoryPeak = 0
			j.Tasks[i].PidsPeak = 0
			j.Tasks[i].Stdout = ""
			j.Tasks[i].Stderr = ""
			j.Tasks[i].JobID = 0
			j.Tasks[i].NodeID = 0
		}
	}

	if !j.Type.IsValid() {
		slog.Error(fmt.Sprintf("Request: POST /api/client/job, Job does not have a valid type: %d, %v", j.Type, j))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Job does not have a valid type"})
		return
	}

	if j.Type == common.JobTypeParallel {
		if j.HostsAlloc <= 0 {
			slog.Error(fmt.Sprintf("Request: POST /api/client/job, Job has invalid value on HostsAlloc: %d, for parallel job, %v", j.HostsAlloc, j))
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Job has invalid value on HostsAlloc"})
			return
		}
	} else {
		j.HostsAlloc = 1
	}

	if j.CoresAlloc <= 0 {
		slog.Error(fmt.Sprintf("Request: POST /api/client/job, Job has invalid value on CoresAlloc: %d, for job, %v", j.CoresAlloc, j))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Job has invalid value on CoresAlloc"})
		return
	}

	if len(j.User) == 0 {
		slog.Error(fmt.Sprintf("Request: POST /api/client/job, Job user is empty, is required: %v", j))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Job user is empty, user required"})
		return
	}

	forUser, err := sql.InsertJob(a.Db, j)
	if err != nil {
		slog.Error(fmt.Sprintf("Failed to insert job: %v, in database: %s", j, err))

		if forUser == 0 {
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: fmt.Sprintf("%s", err)})
			return
		} else if forUser == -1 {
			c.JSON(http.StatusInternalServerError, common.FailResponse{Message: "Internal error"})
			return
		}
	}

	c.JSON(http.StatusOK, common.IdResponse{Id: uint(forUser)})
}

// API for client.
// URL: GET /api/client/task
// Without query parameters all
// tasks are returned.
//
// Query parameters:
// id=x: where x is task id that
// will be returned.
// jobid=x: where x is job id that
// will return all tasks belonging
// to that job.
// running: Return all running tasks.
// Must be the only query parameter
func (a API) clientGetTask(c *gin.Context) {
	if err := validateHost(c, a.Db, false); err != nil {
		slog.Error(fmt.Sprintf("%s", err))
		return
	}

	id := -1
	jobId := -1
	running := false
	tasks := []common.Task{}

	idStr, idExist := c.GetQuery("id")
	if idExist {
		idInt, err := strconv.Atoi(idStr)
		if err != nil {
			slog.Error(fmt.Sprintf("Request: /api/client/task?id=%s, id is not an integer", idStr))
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter id is not an integer"})
			return
		}

		id = idInt
	}

	jobIdStr, jobIdExist := c.GetQuery("jobid")
	if jobIdExist {
		jobIdInt, err := strconv.Atoi(jobIdStr)
		if err != nil {
			slog.Error(fmt.Sprintf("Request: /api/client/task?jobid=%s, jobid is not an integer", jobIdStr))
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter jobid is not an integer"})
			return
		}

		jobId = jobIdInt
	}

	_, rExist := c.GetQuery("running")
	if rExist {
		running = true
	}

	if id >= 0 && jobId >= 0 {
		slog.Warn(fmt.Sprintf("Wrong query parameters: %s from client: %s", c.FullPath(), c.ClientIP()))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Wrong query parameters."})
		return
	}

	if running && (id >= 0 || jobId >= 0) {
		slog.Warn(fmt.Sprintf("Wrong query parameters: %s from client: %s", c.FullPath(), c.ClientIP()))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Wrong query parameters."})
		return
	}

	tasks, err := sql.GetTask(a.Db, id, jobId, running)
	if err != nil {
		slog.Error(fmt.Sprintf("Request: /api/client/task, id=%d, %s", id, err))
		c.JSON(http.StatusInternalServerError, common.FailResponse{Message: "Internal error"})
	} else {
		c.JSON(http.StatusOK, tasks)
	}
}

// API for client.
// URL: GET /api/client/node
// Without query parameters all
// nodes are returned.
//
// Query parameters:
// id=x: where x is task id that
// will be returned.
// tasks: To be used with id,
// will return node with id including
// tasks running on node.
// state=x: name of one of the states from
// common.NodeState, returns all nodes
// with supplied state.
func (a API) clientGetNode(c *gin.Context) {
	if err := validateHost(c, a.Db, false); err != nil {
		slog.Error(fmt.Sprintf("%s", err))
		return
	}

	id := -1
	tasks := false
	state := common.NodeState(-1)
	nodes := []common.Node{}

	idStr, iExist := c.GetQuery("id")
	if iExist {
		idInt, err := strconv.Atoi(idStr)
		if err != nil {
			slog.Error(fmt.Sprintf("Request: /api/client/node?id=%s, id is not an integer", idStr))
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter id is not an integer"})
			return
		}
		id = idInt
	}

	_, tExist := c.GetQuery("tasks")
	if tExist {
		tasks = true
	}

	stateStr, sExist := c.GetQuery("state")
	if sExist {
		switch strings.ToLower(stateStr) {
		case strings.ToLower(common.NodeStateRegister.String()):
			state = common.NodeStateRegister
		case strings.ToLower(common.NodeStateOnline.String()):
			state = common.NodeStateOnline
		case strings.ToLower(common.NodeStateSoftOnline.String()):
			state = common.NodeStateSoftOnline
		case strings.ToLower(common.NodeStateOffline.String()):
			state = common.NodeStateOffline
		case strings.ToLower(common.NodeStateMaintenance.String()):
			state = common.NodeStateMaintenance
		case strings.ToLower(common.NodeStateMaintenanceDrain.String()):
			state = common.NodeStateMaintenanceDrain
		}
	}

	if (id > 0 && state.IsValid()) || (id <= 0 && tasks) {
		slog.Warn(fmt.Sprintf("Wrong query parameters: %s from client: %s", c.FullPath(), c.ClientIP()))
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Wrong query parameters."})
		return
	}

	nodes, err := sql.GetNode(a.Db, id, tasks, state)
	if err != nil {
		slog.Error(fmt.Sprintf("Request: /api/client/node, id=%d, %s", id, err))
		c.JSON(http.StatusInternalServerError, common.FailResponse{Message: "Internal error"})
	} else {
		c.JSON(http.StatusOK, nodes)
	}
}

// API for client.
// URL: PATCH /api/client/node/state
// Parameter id and online is required.
//
// Query parameters:
// id=x: where x is job id that
// should be set online/offline.
// online=x: where x is either true or false
// If value is true then node is set online.
// If value is false node is set offline.
func (a API) clientStateNode(c *gin.Context) {
	if err := validateHost(c, a.Db, false); err != nil {
		slog.Error(fmt.Sprintf("%s", err))
		return
	}

	id := -1
	online := true

	idStr, iExist := c.GetQuery("id")
	if iExist {
		idInt, err := strconv.Atoi(idStr)
		if err != nil {
			slog.Error(fmt.Sprintf("Request: /api/client/node/state?id=%s, id is not an integer", idStr))
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter id is not an integer"})
			return
		}

		id = idInt
	} else {
		slog.Error("Request: /api/client/node/state does not have required parameter: id")
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Parameter id is required"})
		return
	}

	onlineStr, exist := c.GetQuery("online")
	if exist {
		if strings.ToLower(strings.TrimSpace(onlineStr)) == "true" {
			online = true
		} else if strings.ToLower(strings.TrimSpace(onlineStr)) == "false" {
			online = false
		} else {
			c.JSON(http.StatusBadRequest,
				common.FailResponse{Message: fmt.Sprintf("Query parameter online: %s invalid value.", onlineStr)})
			return
		}
	} else {
		c.JSON(http.StatusBadRequest, common.FailResponse{Message: "Query parameter online is required."})
		return
	}

	forUser, err := sql.SetStateNode(a.Db, id, online)
	if err != nil {
		slog.Error(fmt.Sprintf("Request: /api/client/node/state, id=%d, online=%s, %s", id, online, err))
		if forUser {
			c.JSON(http.StatusBadRequest, common.FailResponse{Message: fmt.Sprintf("%s", err)})
			return
		} else {
			c.JSON(http.StatusInternalServerError, common.FailResponse{Message: "Internal error"})
			return
		}
	} else {
		c.JSON(http.StatusOK, gin.H{})
	}
}

// validates host
// checks supplied key argument
// with correctness to database value
func validateHost(c *gin.Context, db *gorm.DB, node bool) error {
	hostIp := c.ClientIP()

	key, kExist := c.GetQuery("key")
	if !kExist || len(key) == 0 {
		c.JSON(http.StatusUnauthorized, gin.H{})
		return fmt.Errorf("Validation failure, host: %s, did not supply variable key in url: %s", hostIp, c.Request.URL.String())
	}

	if node {
		nodes, err := sql.GetNodeKey(db, key)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{})
			return fmt.Errorf("Validation failure, host: %s, %s", hostIp, err)
		}

		nodeExist := false
		for _, n := range nodes {
			if hostIp == n.Ip {
				nodeExist = true
			}
		}

		if !nodeExist {
			c.JSON(http.StatusUnauthorized, gin.H{})
			return fmt.Errorf("Validation failure, host: %s, did not exist in database.", hostIp)
		}
	} else {
		clients, err := sql.GetClientKey(db, key)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{})
			return fmt.Errorf("Validation failure, host: %s, %s", hostIp, err)
		}

		clientExist := false
		for _, c := range clients {
			if hostIp == c.Ip {
				clientExist = true
			}
		}

		if !clientExist {
			c.JSON(http.StatusUnauthorized, gin.H{})
			return fmt.Errorf("Validation failure, host: %s, did not exist in database.", hostIp)
		}
	}

	return nil
}

// Called if URL is invalid
func wrongUrl(c *gin.Context) {
	c.JSON(http.StatusNotFound, gin.H{})
}

// Read and parse config file
func getConfig() daemon.Config {
	viper.SetConfigName("l3qd.conf")
	viper.SetConfigType("toml")
	viper.AddConfigPath(".")
	viper.AddConfigPath("/etc/l3q/")
	viper.SetDefault("port", 39911)
	viper.SetDefault("ip_listen", "127.0.0.1")
	viper.SetDefault("db_file", "/var/lib/l3q/l3qd.db")
	viper.SetDefault("log_file", "/var/log/l3q/l3qd.log")
	viper.SetDefault("cert_directory", "/etc/l3q/certificates")
	viper.SetDefault("queue", "default")
	viper.SetDefault("poll_intervall", 60)
	viper.SetDefault("db_backup.db_backup_directory", "/var/lib/l3q/backup/")
	viper.SetDefault("db_backup.db_backup_hour", 2)
	viper.SetDefault("db_backup.db_backup_minute", 0)
	viper.SetDefault("db_backup.db_backup_max_count", 5)

	err := viper.ReadInConfig()
	if err != nil {
		slog.Error("Failed to read config file: l3qd.conf")
		os.Exit(1)
	}

	var conf daemon.Config

	err = viper.Unmarshal(&conf)
	if err != nil {
		slog.Error(fmt.Sprintf("Failed to parse config file: l3qd.conf, %s", err))
		os.Exit(2)
	}

	switch strings.ToLower(conf.Queue) {
	case "default":
		conf.Queue = "default"
	case "fill":
		conf.Queue = "fill"
	default:
		slog.Error(fmt.Sprintf("Queue: %s is not a valid queue", conf.Queue))
		os.Exit(3)
	}

	if conf.Port <= 0 || conf.Port > 65535 {
		slog.Error(fmt.Sprintf("Port: %d is not a valid port number.", conf.Port))
		os.Exit(3)
	}

	if net.ParseIP(conf.IpListen) == nil {
		slog.Error(fmt.Sprintf("IP addres: %s is not a valid IP address.", conf.IpListen))
		os.Exit(4)
	}

	dbDir := filepath.Dir(conf.DbFile)
	if _, err = os.Stat(dbDir); err != nil {
		if os.IsNotExist(err) {
			slog.Error(fmt.Sprintf("Database directory: %s does not exist.", dbDir))
			os.Exit(5)
		} else {
			slog.Error(fmt.Sprintf("Failed to access database directory: %s, %s", dbDir, err))
			os.Exit(6)
		}
	}

	if conf.PollIntervall < 30 {
		slog.Warn(fmt.Sprintf("Poll intervall: %d is smaller then minimum, setting minimum 30", conf.PollIntervall))
		conf.PollIntervall = 30
	}

	if _, err = os.Stat(conf.DbBackup.Directory); err != nil {
		if os.IsNotExist(err) {
			slog.Warn(fmt.Sprintf("Database backup directory: %s does not exist, will not do database backups.", conf.DbBackup.Directory))
		} else {
			slog.Warn(fmt.Sprintf("Failed to access database backup directory: %s, will not do database backups.", conf.DbBackup.Directory))
		}
	}

	if conf.DbBackup.Hour < 0 || conf.DbBackup.Hour > 23 {
		slog.Warn(fmt.Sprintf("Database backup hour: %d is not valid, will set default: 2", conf.DbBackup.Hour))
		conf.DbBackup.Hour = 2
	}

	if conf.DbBackup.Minute < 0 || conf.DbBackup.Minute > 59 {
		slog.Warn(fmt.Sprintf("Database backup minute: %d is not valid, will set default: 0", conf.DbBackup.Minute))
		conf.DbBackup.Minute = 0
	}

	return conf
}

// Lists all validated node hosts
func listNodeHosts() {
	conf := getConfig()

	db, err := sql.InitDb(conf)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get a handle to database: %s\n", err)
		os.Exit(2)
	}

	nodes, errNode := sql.GetNode(db, -1, false, common.NodeState(-1))
	if errNode != nil {
		fmt.Fprintf(os.Stderr, "Failed to get all nodes from database: %s\n", errNode)
		os.Exit(3)
	}

	noDash := 89
	fmt.Println(strings.Repeat("-", noDash))
	fmt.Printf("|  Id  |       Name      |  Port |")
	fmt.Printf("    IP address   |")
	fmt.Printf("    State    |")
	fmt.Println("       Hostname       |")
	fmt.Println(strings.Repeat("-", noDash))

	for _, n := range nodes {
		fmt.Printf("| %4d |", n.Id)

		if len(n.Name) > 15 {
			fmt.Printf(" %s |", n.Name[:15])
		} else {
			fmt.Printf(" %15s |", n.Name)
		}

		fmt.Printf(" %5d |", n.Port)

		if len(n.Ip) > 15 {
			fmt.Printf(" %15s |", n.Ip[:15])
		} else {
			fmt.Printf(" %15s |", n.Ip)
		}

		if n.State == common.NodeStateMaintenanceDrain {
			fmt.Printf("    Draining |")
		} else {
			fmt.Printf(" %11s |", n.State)
		}

		if len(n.Hostname) > 20 {
			fmt.Printf(" %20s |\n", n.Hostname[:20])
		} else {
			fmt.Printf(" %20s |\n", n.Hostname)
		}

		fmt.Println(strings.Repeat("-", noDash))
	}

}

// Removes node from system
// with specified id.
func rmNode(id int) {
	conf := getConfig()

	db, err := sql.InitDb(conf)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get a handle to database: %s\n", err)
		os.Exit(2)
	}

	if err := sql.RemoveNode(db, id); err != nil {
		fmt.Fprintf(os.Stderr, "Failed to remove node with id: %d\n%s\n", id, err)
	} else {
		fmt.Printf("Node with id: %d, successfully removed\n", id)
	}
}

// Lists all validated client hosts
func listClientHosts() {
	conf := getConfig()

	db, err := sql.InitDb(conf)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get a handle to database: %s\n", err)
		os.Exit(2)
	}

	clients, errC := sql.GetClient(db)
	if errC != nil {
		fmt.Fprintf(os.Stderr, "%s", errC)
		os.Exit(3)
	}

	noDash := 67
	fmt.Println(strings.Repeat("-", noDash))
	fmt.Printf("|  Id  |       Name      |")
	fmt.Printf("    IP address   |")
	fmt.Println("       Hostname       |")
	fmt.Println(strings.Repeat("-", noDash))

	for _, c := range clients {
		fmt.Printf("| %4d |", c.Id)

		if len(c.Name) > 15 {
			fmt.Printf(" %s |", c.Name[:15])
		} else {
			fmt.Printf(" %15s |", c.Name)
		}

		if len(c.Ip) > 15 {
			fmt.Printf(" %15s |", c.Ip[:15])
		} else {
			fmt.Printf(" %15s |", c.Ip)
		}

		if len(c.Hostname) > 20 {
			fmt.Printf(" %20s |\n", c.Hostname[:20])
		} else {
			fmt.Printf(" %20s |\n", c.Hostname)
		}

		fmt.Println(strings.Repeat("-", noDash))
	}
}

// Removes client from system
// with specified id.
func rmClient(id int) {
	conf := getConfig()

	db, err := sql.InitDb(conf)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get a handle to database: %s\n", err)
		os.Exit(2)
	}

	if err := sql.RemoveClient(db, id); err != nil {
		fmt.Fprintf(os.Stderr, "Failed to remove client with id: %d\n%s\n", id, err)
	} else {
		fmt.Printf("Client with id: %d, successfully removed\n", id)
	}
}

// Info about diffferent queues
func queueInfo() {
	fmt.Println("Queues:")
	fmt.Println("")
	fmt.Println("    Default:")
	fmt.Println("    The default queueing system in the l3q daemon")
	fmt.Println("    is similar to a FIFO (first in, first out) stack.")
	fmt.Println("    The first job that is added will be launched first,")
	fmt.Println("    if there are more queued jobs they will all be")
	fmt.Println("    processed in turn from the top. If there are")
	fmt.Println("    enough resources available for the next queued job")
	fmt.Println("    to be launch it will be started, until there are no")
	fmt.Println("    queued jobs left or not enough resources are")
	fmt.Println("    available. All jobs are processed from top to bottom")
	fmt.Println("    and when the first queued job will not fit the processing")
	fmt.Println("    stops, even if there are smaller jobs further down in the")
	fmt.Println("    queue that would be able to be launched this will not")
	fmt.Println("    happen. That job has to wait until all previous queued")
	fmt.Println("    jobs has beed launched first.")
	fmt.Println("    Jobs that are in Depend state are not inluded in the")
	fmt.Println("    queue until dependent jobs are finished.")
	fmt.Println("")
	fmt.Println("    Fill:")
	fmt.Println("    The fill queueing system in the l3q daemon")
	fmt.Println("    is similar to a FIFO (first in, first out) stack and")
	fmt.Println("    that fills up more jobs to make nodes as busy")
	fmt.Println("    as possible. ")
	fmt.Println("    The first job that is added will be launched first,")
	fmt.Println("    if there are more queued jobs they will all be")
	fmt.Println("    processed in turn from the top. If there are")
	fmt.Println("    enough resources available for the next queued job")
	fmt.Println("    to be launch it will be started, until there are no")
	fmt.Println("    queued jobs left or not enough resources are")
	fmt.Println("    available. Jobs further down in the queue will")
	fmt.Println("    be processes to see if they are able to fit in")
	fmt.Println("    available resources. Processing of more jobs")
	fmt.Println("    will finish when no job will fit in available")
	fmt.Println("    resorces.")
	fmt.Println("    All jobs are processed from top to bottom")
	fmt.Println("    and when the first queued job will not fit the processing")
	fmt.Println("    continues to the next job in queue until no more jobs")
	fmt.Println("    will fit.")
	fmt.Println("    Jobs that are in Depend state are not inluded in the")
	fmt.Println("    queue until dependent jobs are finished.")
}

// Prints version
func printVersion() {
	common.AsciiL3q()
	fmt.Printf("l3qd %s\n", common.VERSION)
	fmt.Printf("%s\n", common.COPYRIGHT)
	fmt.Println("License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.")
	fmt.Println("This is free software: you are free to change and redistribute it.")
	fmt.Println("There is NO WARRANTY, to the extent permitted by law.")
	fmt.Println("")
	fmt.Println("Written by Marcus Pedersén.")
}

// Prints help
func printHelp() {
	fmt.Println("Usage: l3qd [OPTION] ...")
	common.AsciiL3q()
	fmt.Println("Execute the l3q daemon. Starts in normal mode without arguments.")
	fmt.Println("")
	fmt.Println("Options:")
	fmt.Println("  --validate-host        Executes the validate daemon.")
	fmt.Println("                         Instructions will be printed at start up.")
	fmt.Println("  --list-node-hosts      List all validated node hosts.")
	fmt.Println("  --list-client-hosts    List all validated client hosts.")
	fmt.Println("  --rm-node-host ID      Removes specified validated node host with specified ID.")
	fmt.Println("  --rm-client-host ID    Removes specified validated client host with specified ID.")
	fmt.Println("  --queue-info           Prints information about available queues.")
	fmt.Println("  --version              Prints version text and exit")
	fmt.Println("  -h, --help             Prints this help text and exit")
	fmt.Println("")
	fmt.Println("More information is available in man pages:")
	fmt.Println("l3q(1), l3q.conf(5), l3qd(8), l3qd.conf(5)")
}

func main() {
	if len(os.Args) > 1 {
		if len(os.Args) == 2 {
			switch os.Args[1] {
			case "--version":
				printVersion()
			case "-h", "--help":
				printHelp()
			case "--validate-host":
				validate.ValidateD(getConfig())
			case "--list-node-hosts":
				listNodeHosts()
			case "--list-client-hosts":
				listClientHosts()
			case "--queue-info":
				queueInfo()
			default:
				fmt.Fprintf(os.Stderr, "Wrong arguments.Try -h or --help for help.\n")
				os.Exit(1)
			}

			os.Exit(0)
		} else if len(os.Args) == 3 {
			if os.Args[1] == "--rm-node-host" {
				if id, err := strconv.Atoi(os.Args[2]); err == nil {
					rmNode(id)
					os.Exit(0)
				} else {
					fmt.Fprintf(os.Stderr, "Failed to parse argument ID: %s, must be positive integer.\n", os.Args[2])
					os.Exit(1)
				}
			} else if os.Args[1] == "--rm-client-host" {
				if id, err := strconv.Atoi(os.Args[2]); err == nil {
					rmClient(id)
					os.Exit(0)
				} else {
					fmt.Fprintf(os.Stderr, "Failed to parse argument ID: %s, must be positive integer.\n", os.Args[2])
					os.Exit(1)
				}
			} else {
				fmt.Fprintf(os.Stderr, "Wrong arguments. Try -h or --help for help.\n")
				os.Exit(1)
			}
		} else {
			fmt.Fprintf(os.Stderr, "Wrong arguments. Try -h or --help for help.\n")
			os.Exit(1)
		}
	}

	conf := getConfig()
	db, err := sql.InitDb(conf)
	if err != nil {
		slog.Error(fmt.Sprintf("%s", err))
		os.Exit(2)
	}

	if err := cert.GenerateCert(conf.CertDir); err != nil {
		slog.Error(fmt.Sprintf("%s", err))
		os.Exit(2)
	}

	// prepare and start worker
	workerEnd := make(chan bool, 1)
	var wait sync.WaitGroup
	wait.Add(1)
	
	go worker.RunWorker(conf, workerEnd, &wait)

	api := API{Db: db}
	route := gin.Default()
	route.NoRoute(wrongUrl)
	route.NoMethod(wrongUrl)
	route.GET("/api/v1/client/job", api.clientGetJob)
	route.GET("/api/v1/client/task", api.clientGetTask)
	route.GET("/api/v1/client/node", api.clientGetNode)
	route.PATCH("/api/v1/client/job/cancel", api.clientCancelJob)
	route.PATCH("/api/v1/client/node/state", api.clientStateNode)
	route.POST("/api/v1/client/job", api.clientPostJob)

	route.RunTLS(fmt.Sprintf("%s:%d", conf.IpListen, conf.Port),
		filepath.Join(conf.CertDir, "cert.pem"),
		filepath.Join(conf.CertDir, "key.pem"))

	// Terminate worker
	// and wait for worker to terminate
	workerEnd <- true
	wait.Wait()
}
